unit CPPlotter;

interface

uses Classes, Controls, Graphics, Messages, Types, Forms, ExtCtrls,
  CPBase, CPView, CPCoordSystem, CPPlotCanvas, CPGraph;

type
  TCPPlotter = class;

  { Describes ongoing mouse interaction. }
  TCPInteractionMode = (
    imNone, //< User is not interacting with the mouse.
    imPan, {< User is panning the view with the mouse, while the left mouse
      button is pressed. }
    imZoom); {< User is zooming the view with the mouse, while the right mouse
      button is pressed. }

  { Event that is triggered, when the mouse moves over a plotter control.
    @param Sender Plotter control instance, which triggered the event.
    @param Shift Mouse and key modifier state
    @param X Horizontal mouse position in client pixel units
    @param Y Vertical mouse position in client pixel units
    @param Coords Mouse position in coordinate units
    @param MouseLeave If true, the mouse cursor left the plotter control }
  TCPPlotMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState;
    X, Y: Integer; Coords: TCPPoint; MouseLeave: Boolean) of object;

  { Event that is triggered, when the view of a plotter control is changed.
    @param Sender Plotter control instance, which triggered the event.
    @param View Describes the current view of the plotter control }
  TCPViewChangeEvent = procedure(Sender: TObject; View: TCPView) of object;

  TCPPlotterView = class;

  { Displays the name and color of a function graph as hint }
  TCPPlotHintWindow = class(THintWindow)
  private
    FPlotter: TCPPlotter;
    FGraph: TCPGraph;
    FShowTimer: TTimer;
    procedure SetGraph(const Value: TCPGraph);
    procedure TimerShow(Sender: TObject);
  protected
  public
    constructor Create(Plotter: TCPPlotter); reintroduce; overload;
    procedure Paint; override;
    procedure ActivateHint(Rect: TRect; const AHint: string); override;
    function CalcHintRect(MaxWidth: Integer; const AHint: string;
      AData: Pointer): TRect; override;
    { Function graph, which the hint refers to }
    property Graph: TCPGraph read FGraph write SetGraph;
  end;

  { Processes keyboard and mouse input and performs operations such as view
    changes on certain mouse and key events. This class handles key and mouse
    input for a plotter control. }
  TCPInputState = class
  private
    { Referenced plotter control }
    FPlotter: TCPPlotter;
    { Referenced view of plotter control }
    FView: TCPView;
    { Describes mode of running mouse interaction }
    FInteractionMode: TCPInteractionMode;
    { Position of mouse cursor in client pixel coordinates, when interaction
      was started }
    FStartCursorPos: TCPPoint;
    { Position of mouse cursor in coordinate system units, when interaction
      was started }
    FStartPos: TCPPoint;
    { Old view center, when interaction mode was started }
    FOldCenter: TCPPoint;
    { Old view zoom, when interaction mode was started }
    FOldZoom: TCPFloat;
    { Position of mouse cursor in client pixel coordinates }
    FCursorPos: TCPPoint;
    { Hint window that is displayed when the mouse cursor is over a graph }
    FHintWindow: TCPPlotHintWindow;
    { Switches to the specified mouse interaction mode }
    procedure StartInteractionMode(InteractionMode: TCPInteractionMode);
    { Stops any ongoing mouse interaction mose }
    procedure StopInteractionMode;
    { Performs view changes while there is an mouse interaction. }
    procedure ProcessInteraction;
    { Displays the names of funciton graphs, when the mouse cursor is over
      them. }
    procedure ProcessGraphHint(X, Y: Integer);
    { Enables or disables fast preview }
    procedure ToggleFastPreview(FastPreview: Boolean);
    { Retrieves the character, which would be printed by the specified key }
    function GetCharFromVirtualKey(Key: Word): string;
  public
    constructor Create(Plotter: TCPPlotter);
    destructor Destroy; override;
    { Adjusts the input state, when the mouse leaves the plotter control. }
    procedure MouseLeaving;
    { Updates and checks mouse state (cursor position and prssed mouse
      buttons. }
    procedure UpdateMouseState(Shift: TShiftState; X, Y: Integer;
      MouseMove: Boolean);
    { Updates and checks the state of the specified key.
      @param Key Key code of pressed or released key
      @param FirstHit True, if the key has been recently hit down. If the key
        is already pressed and pulses periodically, then false is returned. }
    procedure UpdateKeyState(Key: Word; FirstHit: Boolean);
    { Zooms vew by the specfied zoom step. The zoom origin depends on the
      ByMouseWheel, since mouse wheel zoom behavior differs from + and -
      key zoom behavior.
      @param ByMouseWheel If true, the mouse cursor position will be used
        as zoom origin; otherwise the screen center is used as origin.
      @param ZoomStep Zoom step. The final amount of zooming also depends
      on ZoomWheel or ZoomKeypress in TCPlotter.InputOptions. }
    procedure ZoomView(ByMouseWheel: Boolean; ZoomStep: Integer);
    { Pans view by the specified number of steps in horizontal and vertical
      direction. The final amount of panning also deppends on PanKeypress
      defined in TCPlotter.InputOptions. }
    procedure PanView(PanStep: TPoint);
    { Draws guides for interaction mode }
    procedure Draw;
    { Sets or gets view }
    property View: TCPView read FView write FView;
  end;

  { Extends the view class with animated view changes. }
  TCPPlotterView = class(TCPView)
  protected
    { Referenced plotter control }
    FPlotter: TCPPlotter;
    { End zoom value for animated view changes }
    FEndZoom: TCPFloat;
    { End center value for animated view changes }
    FEndCenter: TCPPoint;
    { Start zoom value for animated view changes }
    FStartZoom: TCPFloat;
    { Start center value for animated view changes }
    FStartCenter: TCPPoint;
    { GetTickCount value, when animation was started }
    FAnimStart: Cardinal;
    { Indicates if a view change animaiton is currently running }
    FAnimating: Boolean;
    { If true, the next view change is noit animated. This flag is used to
      disable animation for certain view change routines. }
    FAnimateNext: Boolean;
    { Starts a new animated view change to the specified location. }
    procedure StartAnimation(EndZoom: TCPFloat; EndCenter: TCPVector);
    { Stops an ongoing animated view change at the current position. }
    procedure StopAnimation;
    { Redraws the plotter view with its current view coordinates. }
    procedure DrawFrame;
    { Schedules the frames of the animated view change. }
    procedure ProcessAnimation;
    procedure ViewUpdated; override;
    procedure PlotCanvasChanged; override;
    { Sets a new view by interpolcating between two specified views.
      @param StartZoom Zoom of the start view
      @param EndZoom Zoom of the end view
      @param StartCenter Center of the start view
      @param EndCenter Center of the end view
      @param Progress Transition value between 0 and 1 }
    procedure SetInterpolatedView(StartZoom, EndZoom: TCPFloat;
      StartCenter, EndCenter: TCPPoint; Progress: TCPFloat);
  public
    { Disables animation of the next view change. This is used to disable
      animated view changes for certain user interactions (e.g. panning with
      the mouse). }
    procedure DisableNextAnimation;
    constructor Create(Plotter: TCPPlotter);
    { Changes the view without animation. }
    procedure SetViewAtOnce(Zoom: TCPFloat; YAxisScale: TCPFloat;
      Center: TCPPoint);
    { Sets a new view and performs an animated change to it. This method
      extends the original behaviour by providing animations. If
      DisableNextAnimation is called before, an unanimated view change is
      performed. }
    procedure ChangeView(Zoom: TCPFloat; YAxisScale: TCPFloat;
      Center: TCPPoint); override;
  end;

  { Graph list for plotter control }
  TCPPlotterGraphList = class(TCPGraphList)
  private
    FPlotter: TCPPlotter;
  protected
    { Repaints plotter control on graph changes }
    procedure GraphListChanged; override;
  public
    { Creates a new graph list.
      @param Plotter Plotter control using this ionstance
      @param View Referenced Plotter view }
    constructor Create(Plotter: TCPPlotter; View: TCPView);
  end;

  { Control for plotting function graphs }
  TCPPlotter = class(TCustomControl)
  private
    { Describes view region on coordinate system }
    FView: TCPView;
    { Draws coordinate system }
    FCoordSystem: TCPCoordSystem;
    { Handles input state  for plotter control }
    FInputState: TCPInputState;
    { Describes visual properties for plotting }
    FPlotStyle: TCPPlotStyle;
    { Describes input related properties for user interactions }
    FInputOptions: TCPInputOptions;
    { Manages a list of graphs that can be plotted }
    FGraphs: TCPGraphList;
    { Controls the display size of text, coordinate grid as well as line
      thickness. }
    FDisplayZoom: TCPFloat;
    { Plot mouse move event }
    FOnPlotMouseMove: TCPPlotMouseMoveEvent;
    { View change event }
    FOnViewChange: TCPViewChangeEvent;
    procedure SetPlotStyle(const Value: TCPPlotStyle);
    procedure SetOnPlotMouseMove(const Value: TCPPlotMouseMoveEvent);
    procedure SetView(const Value: TCPView);
    procedure SetDisplayZoom(const Value: TCPFloat);
    { Triggers the OnViewChange event. }
    procedure DoViewChange(View: TCPView);
  protected
    procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
    { Is called, after the view has been changed. }
    procedure ViewUpdated;
    { Notifies view about new canvas bounds. }
    procedure UpdateCanvasBounds;
    { Handles key down messages }
    procedure CNKeyDown(var Message: TWMKeyDown); message CN_KEYDOWN;
    { Handles mouse down messages }
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    { Handles mouse move messages }
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    { Handles mouse up messages }
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    { Handles mouse leave messages }
    procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
    { Handles timerr messages for animated view changes }
    procedure WMTimer(var Message: TWMTimer); message WM_TIMER;
    { Sets a new parent window }
    procedure SetParent(AParent: TWinControl); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    { Assign plot style, input options and view from another plotter control }
    procedure Assign(Source: TPersistent); override;
    { Reacts on plotter control size changes }
    procedure Resize; override;
    { Performs drawing of the plotter control. }
    procedure Paint; override;
    { Performs drawing using the specified view instance. This can also be used
      to export images, when the specified view uses a sub class of
      TCPPlotExport as attached canvas. }
    procedure PaintToView(View: TCPView);
    { Processes mouse wheel events }
    procedure MouseWheelHandler(var Message: TMessage); override;
    { Control canvas }
    property Canvas;
    { Sets or gets view on the coordinate system }
    property View: TCPView read FView write SetView;
    { Sets or gets plotting style related attributes }
    property PlotStyle: TCPPlotStyle read FPlotStyle write SetPlotStyle;
    { Sets or gets input related properties for user interactions }
    property InputOptions: TCPInputOptions read FInputOptions
      write FInputOptions;
    { Provides access to function graphs }
    property Graphs: TCPGraphList read FGraphs write FGraphs;
    { Coordinate system }
    property CoordSystem: TCPCoordSystem read FCoordSystem;
    { Event that is triggered, when the mouse moves over a plotter control. }
    property OnPlotMouseMove: TCPPlotMouseMoveEvent read FOnPlotMouseMove
      write SetOnPlotMouseMove;
    { Event that is triggered, when the plotter control view is modified
      either by user interactions or by the method calls. }
    property OnViewChange: TCPViewChangeEvent read FOnViewChange
      write FOnViewChange;
    { Sets or gets the display size of text, coordinate grid as well as line
      thickness. }
    property DisplayZoom: TCPFloat read FDisplayZoom write SetDisplayZoom;
  published
    property Font;
  end;

implementation

uses SysUtils, CPGDIPlotCanvas, CPGDIPlusPlotCanvas, Windows, Math, Themes,
     CPPlotExport;

{ TCPPlotter }

procedure TCPPlotter.Assign(Source: TPersistent);
var
  APlotter: TCPPlotter;
begin
  if (Source is TCPPlotter) then
  begin
    APlotter := Source as TCPPLotter;
    PlotStyle := APlotter.PlotStyle;
    FInputOptions := APlotter.InputOptions;
    FView.ChangeView(APlotter.View.Zoom, APlotter.View.YAxisScale,
      APlotter.View.Center);
    Font := APlotter.Font;
  end
  else
    inherited Assign(Source);
end;

procedure TCPPlotter.CMFontChanged(var Message: TMessage);
begin
  if Assigned(FView.PlotCanvas) then
  begin
    FView.PlotCanvas.Font := Font;
    Invalidate;
  end;
end;

procedure TCPPlotter.CMMouseLeave(var Message: TMessage);
begin
  FInputState.MouseLeaving;
  if (Assigned(FOnPlotMouseMove)) then
    FOnPlotMouseMove(self, [], 0, 0, CPToVector(0.0, 0.0), true);
end;

procedure TCPPlotter.CNKeyDown(var Message: TWMKeyDown);
begin
  if (Message.KeyData shr 30 = 0) then // Has key been hit?
    FInputState.UpdateKeyState(Message.CharCode, true)
  else
    FInputState.UpdateKeyState(Message.CharCode, false);
end;

constructor TCPPlotter.Create(AOwner: TComponent);
begin
  inherited;
  ControlStyle := [];
  DragMode := dmManual;
  DockSite := false;
  UseDockManager := false;

  FPlotStyle := CPDefaultStyle;
  FDisplayZoom := 1.0;
  FInputOptions := CPDefaultInputOptions;
  Color := FPlotStyle.Colors.Background;
  FView := TCPPlotterView.Create(self);
  FCoordSystem := TCPCoordSystem.Create(FView, @FPlotStyle);
  FInputState := TCPInputState.Create(self);
  FGraphs := TCPPlotterGraphList.Create(self, FView);
  FGraphs.PlotStyleUpdated(FPlotStyle);
  ControlStyle := [csCaptureMouse];
end;

destructor TCPPlotter.Destroy;
begin
  FGraphs.Free;
  FInputState.Free;
  FCoordSystem.Free;
  FView.Free;
  inherited;
end;

procedure TCPPlotter.DoViewChange(View: TCPView);
begin
  if (Assigned(FOnViewChange)) then
    FOnViewChange(self, FView);
end;

procedure TCPPlotter.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited;
  SetFocus;
  FInputState.UpdateMouseState(Shift, X, Y, false);
end;

procedure TCPPlotter.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
  inherited;
  FInputState.UpdateMouseState(Shift, X, Y, true);
  if (Assigned(FOnPlotMouseMove)) then
    FOnPlotMouseMove(self, Shift, X, Y, FView.ToCoord(CPToVector(X, Y)), false);
end;

procedure TCPPlotter.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
var
  OutOfControl: Boolean;
begin
  inherited;
  FInputState.UpdateMouseState(Shift, X, Y, false);
  // Trigger event, when mouse is out of control bounds:
  OutOfControl := (not InRange(X, 0, Width)) or (not InRange(Y, 0, Height));
  if (OutOfControl and Assigned(FOnPlotMouseMove)) then
    FOnPlotMouseMove(self, [], 0, 0, CPToVector(0.0, 0.0), true);
end;

procedure TCPPlotter.MouseWheelHandler(var Message: TMessage);
begin
  inherited;
  case Message.Msg of
    WM_MOUSEWHEEL:
      FInputState.ZoomView(true, TWMMouseWheel(Message).WheelDelta div 120);
  end;
end;

procedure TCPPlotter.Paint;
var
  PlotCanvas: TCPPlotCanvas;
begin
  PlotCanvas := FView.PlotCanvas;
  PlotCanvas.Color := Color;
  PlotCanvas.BeginDraw;
  FCoordSystem.DrawGrid;
  FGraphs.Draw;
  FCoordSystem.DrawAxes;
  FInputState.Draw;
  PlotCanvas.EndDraw;
end;

procedure TCPPlotter.PaintToView(View: TCPView);
var
  OldView: TCPView;
begin
  OldView := FView;
  Self.View := View;
  Paint;
  Self.View := OldView;
end;

procedure TCPPlotter.Resize;
begin
  inherited;
  UpdateCanvasBounds;
end;

procedure TCPPlotter.SetDisplayZoom(const Value: TCPFloat);
var
  DPI: Integer;
begin
  if (Value <> FDisplayZoom) then
  begin
    FDisplayZoom := Value;
    DPI := GetDeviceCaps(Canvas.Handle, LOGPIXELSX);
    View.PlotCanvas.UnitsPerInch := Value * DPI;
    FCoordSystem.Rebuild;
    Invalidate;
  end;
end;

procedure TCPPlotter.SetOnPlotMouseMove(
  const Value: TCPPlotMouseMoveEvent);
begin
  FOnPlotMouseMove := Value;
end;

procedure TCPPlotter.SetParent(AParent: TWinControl);
begin
  inherited;
  if (FView.PlotCanvas = nil) then
    FView.PlotCanvas := TCPGDIPlotCanvas.Create(self);
end;

procedure TCPPlotter.SetPlotStyle(const Value: TCPPlotStyle);
begin
  FPlotStyle := Value;
  {for I := 0 to High(FPlotStyle.Colors.Colors) do
    FPlotStyle.Colors.Colors[I] := ColorToRGB(FPlotStyle.Colors.Colors[I]);}
  Color := FPlotStyle.Colors.Background;

  if (not FPlotStyle.UseAnimation) and (FView is TCPPlotterView) then
    TCPPlotterView(FView).StopAnimation; // Stop running animation
  if (FPlotStyle.FastPreviewMode = false) then
  begin
    FCoordSystem.FastPreview := false;
    FGraphs.FastPreview := false;
  end;

  FGraphs.PlotStyleUpdated(FPlotStyle);
  ViewUpdated;
end;

procedure TCPPlotter.SetView(const Value: TCPView);
begin
  if (FView <> Value) then
  begin
    FView := Value;
    FView.PlotCanvas.Font := Font; // Assign font to new view
    FCoordSystem.View := FView;
    FGraphs.View := FView;
    FInputState.View := FView;
    ViewUpdated;
  end;
end;

procedure TCPPlotter.UpdateCanvasBounds;
begin
  if Assigned(FView.PlotCanvas) then
    FView.SetCanvasBounds(CPToBounds(0, 0, ClientWidth, ClientHeight));
end;

procedure TCPPlotter.ViewUpdated;
begin
  DoViewChange(FView);
  FGraphs.InvalidateGraphShapes;
  FCoordSystem.Rebuild;
  Invalidate;
end;

procedure TCPPlotter.WMTimer(var Message: TWMTimer);
begin
  TCPPlotterView(FView).ProcessAnimation;
end;


{ TCPInputState }

procedure TCPInputState.ZoomView(ByMouseWheel: Boolean; ZoomStep: Integer);
begin
  if (ByMouseWheel) then
    FView.ZoomTo(FView.ToCoord(CPToVector(FCursorPos.X, FCursorPos.Y)),
      FView.Zoom + FPlotter.FInputOptions.ZoomWheel * ZoomStep)
  else
    FView.ZoomView(FPlotter.FInputOptions.ZoomKeypress * ZoomStep);
end;

constructor TCPInputState.Create(Plotter: TCPPlotter);
begin
  FPlotter := Plotter;
  FView := Plotter.View;
  FInteractionMode := imNone;
  FHintWindow := TCPPlotHintWindow.Create(FPlotter);
end;

procedure TCPInputState.ProcessInteraction;
var
  ZoomDelta: Double;
begin
  if (FInteractionMode <> imNone) then
    (FView as TCPPlotterView).DisableNextAnimation;
  case FInteractionMode of
    imPan:
    begin
      FView.PanTo(CPAdd(FOldCenter,
      CPSub(FView.ToCoord(FStartCursorPos), FView.ToCoord(FCursorPos))));
    end;
    imZoom:
    begin
      ZoomDelta := (FStartCursorPos.Y - FCursorPos.Y) *
        FPlotter.FInputOptions.ZoomMouse;
      FView.ZoomTo(FStartPos, FOldZoom + ZoomDelta);
    end;
  end;
end;

procedure TCPInputState.StartInteractionMode(
  InteractionMode: TCPInteractionMode);
begin
  FStartCursorPos := CPToVector(FCursorPos.X, FCursorPos.Y);
  FStartPos := FView.ToCoord(FStartCursorPos);
  FInteractionMode := InteractionMode;
  FOldCenter := FView.Center;
  FOldZoom := FView.Zoom;
  case InteractionMode of
    imPan: FPlotter.Cursor := crSizeAll;
    imZoom: FPlotter.Cursor := crSizeNS;
  end;
  if (FInteractionMode <> imNone) then
    ToggleFastPreview(true);
end;

procedure TCPInputState.StopInteractionMode;
begin
  if (FInteractionMode <> imNone) then
    ToggleFastPreview(false);
  FInteractionMode := imNone;
  FPlotter.Cursor := crDefault;
  FPlotter.Invalidate;
end;

procedure TCPInputState.UpdateMouseState(Shift: TShiftState; X, Y: Integer;
  MouseMove: Boolean);
begin
  if (MouseMove) then
    ProcessGraphHint(X, Y);
  FCursorPos := CPToVector(X, Y);
  ProcessInteraction;
  case FInteractionMode of
  imNone:
    if Shift = [ssLeft] then
      StartInteractionMode(imPan)
    else if (Shift = [ssRight]) and (not MouseMove) then
      // No zoom interaction in mouse move events
      StartInteractionMode(imZoom);
  imPan:
    if (not (ssLeft in Shift)) then
      StopInteractionMode;
  imZoom:
    if (not (ssRight in Shift)) then
      StopInteractionMode;
  end;
end;

procedure TCPInputState.PanView(PanStep: TPoint);
var
  PanFactor: TCPVector;
begin
  PanFactor.X := PanStep.X * FPlotter.FInputOptions.PanKeypress;
  PanFactor.Y := PanStep.Y * FPlotter.FInputOptions.PanKeypress;
  FView.PanView(PanFactor);
end;

procedure TCPInputState.UpdateKeyState(Key: Word; FirstHit: Boolean);
var
  PanStep: TPoint;
  KeyChar: string;
begin
  KeyChar := GetCharFromVirtualKey(Key);
  PanStep := Point(0, 0);
  if KeyChar = '+' then
    ZoomView(false, 1)
  else if KeyChar = '-' then
    ZoomView(false, -1);
  case Key of
    VK_SPACE: if FirstHit then FView.ResetView;
    VK_RIGHT: Inc(PanStep.X);
    VK_LEFT: Dec(PanStep.X);
    VK_UP: Inc(PanStep.Y);
    VK_DOWN: Dec(PanStep.Y);
  end;
  if (PanStep.X <> 0) or (PanStep.Y <> 0) then
    PanView(PanStep);
end;

procedure TCPInputState.Draw;
const
  FocusCrossSize = 16;
var
  Point: TCPPoint;
  PlotCanvas: TCPPlotCanvas;
  Size: TCPFloat;
  Text: string;
begin
  PlotCanvas := FView.PlotCanvas;
  if (FInteractionMode = imZoom) then
  begin
    Point := FView.ToCanvas(FStartPos);
    Size := PlotCanvas.SmoothDistance(PlotCanvas.PtToUnits(FocusCrossSize / 2));
    PlotCanvas.PenColor := FPlotter.PlotStyle.Colors.Guides;
    PlotCanvas.MoveTo(Point.X - Size, Point.Y);
    PlotCanvas.LineTo(Point.X + Size, Point.Y);
    PlotCanvas.MoveTo(Point.X, Point.Y - Size);
    PlotCanvas.LineTo(Point.X, Point.Y + Size);
    Text := Format('%.1f x', [Fview.Zoom - FOldZoom]);
    if (Text[1] <> '-') then
      Insert('+', Text, 1);
    Point := CPToVector(
      PlotCanvas.SmoothDistance(Point.X + PlotCanvas.PtToUnits(10)),
      PlotCanvas.SmoothDistance(Point.Y + PlotCanvas.PtToUnits(10)));
    PlotCanvas.TextOut(Point.X, Point.Y, Text);
  end;
end;

destructor TCPInputState.Destroy;
begin
  FHintWindow.Free;
  inherited;
end;

procedure TCPInputState.ProcessGraphHint(X, Y: Integer);
var
  Pt, HintPos: TPoint;
  Graph: TCPGraph;
  R: TRect;
  MoveOnly: Boolean;
begin
  GetCursorPos(Pt);
  Graph := FPlotter.Graphs.GetGraphNearPoint(CPToVector(X, Y));
  if (Graph = nil) or (FInteractionMode <> imNone) then
  begin
    if (FHintWindow.HandleAllocated) then
      FHintWindow.ReleaseHandle;
    exit;
  end;
  // Only show the hint if ShowHint is true and it differs from
  // the last hint we showed.
  if (FPlotter.ShowHint) then
  begin
    MoveOnly := (FHintWindow.Graph = Graph) and (FHintWindow.HandleAllocated);
    if (MoveOnly) then
      //FHintWindow.ReleaseHandle
      R := Bounds(0, 0, FHintWindow.Width, FHintWindow.Height)
    else begin
      R := FHintWindow.CalcHintRect(Screen.Width, Graph.Caption, nil);
      FHintWindow.Graph := Graph;
    end;
    HintPos := Point(Pt.X, Pt.Y + 20);
    if (MoveOnly) then
    begin
      HintPos.X := Min(Screen.Width - R.Right, HintPos.X);
      HintPos.Y := Min(Screen.Height - R.Bottom, HintPos.Y);
      {FHintWindow.SetBounds(HintPos.X, HintPos.Y, FHintWindow.Width,
      FHintWindow.Height);}
    end
    else begin
      OffsetRect(R, HintPos.X, HintPos.Y);
      FHintWindow.ActivateHint(R, Graph.Caption);
    end;
  end;
end;

procedure TCPInputState.MouseLeaving;
begin
  StopInteractionMode;
  FHintWindow.ReleaseHandle;
end;

procedure TCPInputState.ToggleFastPreview(FastPreview: Boolean);
begin
  if (FPlotter.FPlotStyle.FastPreviewMode) then
  begin
    FPlotter.CoordSystem.FastPreview := FastPreview;
    FPlotter.Graphs.FastPreview := FastPreview;
  end;
end;

function TCPInputState.GetCharFromVirtualKey(Key: Word): string;
var
   KeyboardState: TKeyboardState;
   AsciiResult: Integer;
begin
   GetKeyboardState(KeyboardState) ;
   SetLength(Result, 2) ;
   AsciiResult := ToAscii(Key, MapVirtualKey(Key, 0), KeyboardState,
     @Result[1], 0) ;
   SetLength(Result, AsciiResult);
end;

{ TCPPlotterView }

constructor TCPPlotterView.Create(Plotter: TCPPlotter);
begin
  inherited Create;
  FPlotter := Plotter;
  FAnimating := false;
end;

procedure TCPPlotterView.DisableNextAnimation;
begin
  FAnimateNext := false;
end;

procedure TCPPlotterView.DrawFrame;
begin
  ViewUpdated;
  FPlotter.Invalidate;
end;

procedure TCPPlotterView.ProcessAnimation;
var
  T, Progress: Single;
begin
  if (FAnimating) then
  begin
    T := (GetTickCount - FAnimStart) / FPlotter.PlotStyle.AnimDuration;
    if T > 1 then
      Progress := 1
    else
      Progress := IntPower(T - 1, 3) + 1;//Cos((T - 1) * Pi/2);
    SetTimer(FPlotter.Handle, 1, FPlotter.PlotStyle.AnimFrameDelay, nil);
    SetInterpolatedView(FStartZoom, FEndZoom,
      FStartCenter, FEndCenter, Progress);
    //SetViewAtOnce(AZoom, FYAxisScale, ACenter);
    if (T > 1) then
    begin
      StopAnimation;
      KillTimer(FPlotter.Handle, 1);
    end;
  end;
end;

procedure TCPPlotterView.SetInterpolatedView(StartZoom, EndZoom: TCPFloat;
      StartCenter, EndCenter: TCPPoint; Progress: TCPFloat);
var
  ACenter: TCPPoint;
  AZoom: TCPFloat;
begin
  ACenter.X := CPLerp(StartCenter.X, EndCenter.X, Progress);
  ACenter.Y := CPLerp(StartCenter.Y, EndCenter.Y, Progress);
  //AZoom := CPLerp(StartZoom, EndZoom, Progress);
  AZoom := ScaleToZoom(CPLerp(
    ZoomToScale(StartZoom), ZoomToScale(EndZoom), Progress));
  SetViewAtOnce(AZoom, FYAxisScale, ACenter);
end;

procedure TCPPlotterView.PlotCanvasChanged;
begin
  if (Assigned(FPlotCanvas)) then
  begin
    if (Assigned(FPlotter.Parent)) and (FPlotter.ClientWidth > 0)
      and (FPlotter.ClientHeight > 0) then
      FPlotter.UpdateCanvasBounds; // Resize canvas and repaint plotter control
    FPlotCanvas.Font := FPlotter.Font;
  end;
end;

procedure TCPPlotterView.ChangeView(Zoom, YAxisScale: TCPFloat;
  Center: TCPPoint);
begin
  if (FPlotter.PlotStyle.UseAnimation) and (FAnimateNext) then
  begin
    FYAxisScale := YAxisScale;
    StartAnimation(Zoom, Center);
  end
  else
  begin
    if (FPlotter.PlotStyle.UseAnimation) and (not FAnimateNext) and
      (FAnimating) then
      StopAnimation;
    SetViewAtOnce(Zoom, YAxisScale, Center);
  end;
  FAnimateNext := true;
end;

procedure TCPPlotterView.SetViewAtOnce(Zoom, YAxisScale: TCPFloat;
  Center: TCPPoint);
begin
  inherited ChangeView(Zoom, YAxisScale, Center);
  ViewUpdated;
end;

procedure TCPPlotterView.StartAnimation(EndZoom: TCPFloat;
  EndCenter: TCPVector);
begin
  FEndZoom := EndZoom;
  FEndCenter := EndCenter;
  FStartZoom := FZoom;
  FStartCenter := FCenter;
  FAnimStart := GetTickCount;
  FAnimating := true;
  SetTimer(FPlotter.Handle, 1, FPlotter.PlotStyle.AnimFrameDelay, nil);
  FPlotter.FInputState.ToggleFastPreview(true);
end;

procedure TCPPlotterView.StopAnimation;
begin
  FAnimating := false;
  FPlotter.FInputState.ToggleFastPreview(false);
end;

procedure TCPPlotterView.ViewUpdated;
begin
  inherited;
  FPlotter.ViewUpdated;
end;

{ TCPPlotterGraphList }

constructor TCPPlotterGraphList.Create(Plotter: TCPPlotter; View: TCPView);
begin
  FPlotter := Plotter;
  inherited Create(View);
end;

procedure TCPPlotterGraphList.GraphListChanged;
begin
  FPlotter.Invalidate;
end;

{ TCPPlotHintWindow }

function TCPPlotHintWindow.CalcHintRect(MaxWidth: Integer;
  const AHint: string; AData: Pointer): TRect;
var
  Size: TSize;
begin
  Canvas.Font := Font;
  Size := Canvas.TextExtent(AHint);
  Result := Bounds(0, 0, Size.cx + 28, Size.cy + 3);
end;

constructor TCPPlotHintWindow.Create(Plotter: TCPPlotter);
begin
  inherited Create(Plotter);
  FPlotter := Plotter;
  FShowTimer := TTimer.Create(self);
  FShowTimer.OnTimer := TimerShow;
  Visible := false;
  Color := clInfoBk;
  Font.Handle := Screen.HintFont.Handle;
  DoubleBuffered := true;
end;

procedure TCPPlotHintWindow.ActivateHint(Rect: TRect; const AHint: string);
begin
  Caption := AHint;
  Inc(Rect.Bottom, 4);
  UpdateBoundsRect(Rect);
  if Rect.Top + Height > Screen.DesktopHeight then
    Rect.Top := Screen.DesktopHeight - Height;
  if Rect.Left + Width > Screen.DesktopWidth then
    Rect.Left := Screen.DesktopWidth - Width;
  if Rect.Left < Screen.DesktopLeft then Rect.Left := Screen.DesktopLeft;
  if Rect.Top < Screen.DesktopTop then Rect.Top := Screen.DesktopTop;
  SetWindowPos(Handle, HWND_TOPMOST, Rect.Left, Rect.Top, Width, Height,
    SWP_NOACTIVATE);
  ParentWindow := Application.Handle;
  FShowTimer.Enabled := false;
  FShowTimer.Enabled := true;
  FShowTimer.Interval := 50;
end;



procedure TCPPlotHintWindow.Paint;
var
  ACanvas: TCanvas;
  R: TRect;
  Y: Integer;
begin
  ACanvas := Canvas;
  R := ClientRect;
  ACanvas.Brush.Color := clInfoBk;
  ACanvas.TextRect(R, 22, 2, Caption);
  ACanvas.Brush.Color := FGraph.Color;
  ACanvas.Pen.Color := clBlack;
  Y := (Height - 12) div 2;
  R := Bounds(R.Left + 3, R.Top + Y - 1, 12, 12);
  ACanvas.Rectangle(R);
end;

procedure TCPPlotHintWindow.SetGraph(const Value: TCPGraph);
begin
  FGraph := Value;
end;

procedure TCPPlotHintWindow.TimerShow(Sender: TObject);
begin
  if (HandleAllocated) then
  begin
    ShowWindow(Handle, SW_SHOWNOACTIVATE);
    Invalidate;
  end;
end;

end.
